/*!
 * # Semantic UI - Search
 * http://github.com/semantic-org/semantic-ui/
 *
 *
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

(function($, window, document, undefined) {
  "use strict";

  window =
    typeof window != "undefined" && window.Math == Math
      ? window
      : typeof self != "undefined" && self.Math == Math
        ? self
        : Function("return this")();

  $.fn.search = function(parameters) {
    var $allModules = $(this),
      moduleSelector = $allModules.selector || "",
      time = new Date().getTime(),
      performance = [],
      query = arguments[0],
      methodInvoked = typeof query == "string",
      queryArguments = [].slice.call(arguments, 1),
      returnedValue;
    $(this).each(function() {
      var settings = $.isPlainObject(parameters)
          ? $.extend(true, {}, $.fn.search.settings, parameters)
          : $.extend({}, $.fn.search.settings),
        className = settings.className,
        metadata = settings.metadata,
        regExp = settings.regExp,
        fields = settings.fields,
        selector = settings.selector,
        error = settings.error,
        namespace = settings.namespace,
        eventNamespace = "." + namespace,
        moduleNamespace = namespace + "-module",
        $module = $(this),
        $prompt = $module.find(selector.prompt),
        $searchButton = $module.find(selector.searchButton),
        $results = $module.find(selector.results),
        $result = $module.find(selector.result),
        $category = $module.find(selector.category),
        element = this,
        instance = $module.data(moduleNamespace),
        disabledBubbled = false,
        resultsDismissed = false,
        module;

      module = {
        initialize: function() {
          module.verbose("Initializing module");
          module.get.settings();
          module.determine.searchFields();
          module.bind.events();
          module.set.type();
          module.create.results();
          module.instantiate();
        },
        instantiate: function() {
          module.verbose("Storing instance of module", module);
          instance = module;
          $module.data(moduleNamespace, module);
        },
        destroy: function() {
          module.verbose("Destroying instance");
          $module.off(eventNamespace).removeData(moduleNamespace);
        },

        refresh: function() {
          module.debug("Refreshing selector cache");
          $prompt = $module.find(selector.prompt);
          $searchButton = $module.find(selector.searchButton);
          $category = $module.find(selector.category);
          $results = $module.find(selector.results);
          $result = $module.find(selector.result);
        },

        refreshResults: function() {
          $results = $module.find(selector.results);
          $result = $module.find(selector.result);
        },

        bind: {
          events: function() {
            module.verbose("Binding events to search");
            if (settings.automatic) {
              $module.on(
                module.get.inputEvent() + eventNamespace,
                selector.prompt,
                module.event.input
              );
              $prompt.attr("autocomplete", "off");
            }
            $module
              // prompt
              .on("focus" + eventNamespace, selector.prompt, module.event.focus)
              .on("blur" + eventNamespace, selector.prompt, module.event.blur)
              .on(
                "keydown" + eventNamespace,
                selector.prompt,
                module.handleKeyboard
              )
              // search button
              .on("click" + eventNamespace, selector.searchButton, module.query)
              // results
              .on(
                "mousedown" + eventNamespace,
                selector.results,
                module.event.result.mousedown
              )
              .on(
                "mouseup" + eventNamespace,
                selector.results,
                module.event.result.mouseup
              )
              .on(
                "click" + eventNamespace,
                selector.result,
                module.event.result.click
              );
          }
        },

        determine: {
          searchFields: function() {
            // this makes sure $.extend does not add specified search fields to default fields
            // this is the only setting which should not extend defaults
            if (parameters && parameters.searchFields !== undefined) {
              settings.searchFields = parameters.searchFields;
            }
          }
        },

        event: {
          input: function() {
            if (settings.searchDelay) {
              clearTimeout(module.timer);
              module.timer = setTimeout(function() {
                if (module.is.focused()) {
                  module.query();
                }
              }, settings.searchDelay);
            } else {
              module.query();
            }
          },
          focus: function() {
            module.set.focus();
            if (settings.searchOnFocus && module.has.minimumCharacters()) {
              module.query(function() {
                if (module.can.show()) {
                  module.showResults();
                }
              });
            }
          },
          blur: function(event) {
            var pageLostFocus = document.activeElement === this,
              callback = function() {
                module.cancel.query();
                module.remove.focus();
                module.timer = setTimeout(
                  module.hideResults,
                  settings.hideDelay
                );
              };
            if (pageLostFocus) {
              return;
            }
            resultsDismissed = false;
            if (module.resultsClicked) {
              module.debug("Determining if user action caused search to close");
              $module.one(
                "click.close" + eventNamespace,
                selector.results,
                function(event) {
                  if (module.is.inMessage(event) || disabledBubbled) {
                    $prompt.focus();
                    return;
                  }
                  disabledBubbled = false;
                  if (!module.is.animating() && !module.is.hidden()) {
                    callback();
                  }
                }
              );
            } else {
              module.debug(
                "Input blurred without user action, closing results"
              );
              callback();
            }
          },
          result: {
            mousedown: function() {
              module.resultsClicked = true;
            },
            mouseup: function() {
              module.resultsClicked = false;
            },
            click: function(event) {
              module.debug("Search result selected");
              var $result = $(this),
                $title = $result.find(selector.title).eq(0),
                $link = $result.is("a[href]")
                  ? $result
                  : $result.find("a[href]").eq(0),
                href = $link.attr("href") || false,
                target = $link.attr("target") || false,
                title = $title.html(),
                // title is used for result lookup
                value = $title.length > 0 ? $title.text() : false,
                results = module.get.results(),
                result =
                  $result.data(metadata.result) ||
                  module.get.result(value, results),
                returnedValue;
              if ($.isFunction(settings.onSelect)) {
                if (
                  settings.onSelect.call(element, result, results) === false
                ) {
                  module.debug(
                    "Custom onSelect callback cancelled default select action"
                  );
                  disabledBubbled = true;
                  return;
                }
              }
              module.hideResults();
              if (value) {
                module.set.value(value);
              }
              if (href) {
                module.verbose("Opening search link found in result", $link);
                if (target == "_blank" || event.ctrlKey) {
                  window.open(href);
                } else {
                  window.location.href = href;
                }
              }
            }
          }
        },
        handleKeyboard: function(event) {
          var // force selector refresh
            $result = $module.find(selector.result),
            $category = $module.find(selector.category),
            $activeResult = $result.filter("." + className.active),
            currentIndex = $result.index($activeResult),
            resultSize = $result.length,
            hasActiveResult = $activeResult.length > 0,
            keyCode = event.which,
            keys = {
              backspace: 8,
              enter: 13,
              escape: 27,
              upArrow: 38,
              downArrow: 40
            },
            newIndex;
          // search shortcuts
          if (keyCode == keys.escape) {
            module.verbose("Escape key pressed, blurring search field");
            module.hideResults();
            resultsDismissed = true;
          }
          if (module.is.visible()) {
            if (keyCode == keys.enter) {
              module.verbose("Enter key pressed, selecting active result");
              if ($result.filter("." + className.active).length > 0) {
                module.event.result.click.call(
                  $result.filter("." + className.active),
                  event
                );
                event.preventDefault();
                return false;
              }
            } else if (keyCode == keys.upArrow && hasActiveResult) {
              module.verbose("Up key pressed, changing active result");
              newIndex = currentIndex - 1 < 0 ? currentIndex : currentIndex - 1;
              $category.removeClass(className.active);
              $result
                .removeClass(className.active)
                .eq(newIndex)
                .addClass(className.active)
                .closest($category)
                .addClass(className.active);
              event.preventDefault();
            } else if (keyCode == keys.downArrow) {
              module.verbose("Down key pressed, changing active result");
              newIndex =
                currentIndex + 1 >= resultSize
                  ? currentIndex
                  : currentIndex + 1;
              $category.removeClass(className.active);
              $result
                .removeClass(className.active)
                .eq(newIndex)
                .addClass(className.active)
                .closest($category)
                .addClass(className.active);
              event.preventDefault();
            }
          } else {
            // query shortcuts
            if (keyCode == keys.enter) {
              module.verbose("Enter key pressed, executing query");
              module.query();
              module.set.buttonPressed();
              $prompt.one("keyup", module.remove.buttonFocus);
            }
          }
        },

        setup: {
          api: function(searchTerm, callback) {
            var apiSettings = {
                debug: settings.debug,
                on: false,
                cache: settings.cache,
                action: "search",
                urlData: {
                  query: searchTerm
                },
                onSuccess: function(response) {
                  module.parse.response.call(element, response, searchTerm);
                  callback();
                },
                onFailure: function() {
                  module.displayMessage(error.serverError);
                  callback();
                },
                onAbort: function(response) {},
                onError: module.error
              },
              searchHTML;
            $.extend(true, apiSettings, settings.apiSettings);
            module.verbose("Setting up API request", apiSettings);
            $module.api(apiSettings);
          }
        },

        can: {
          useAPI: function() {
            return $.fn.api !== undefined;
          },
          show: function() {
            return (
              module.is.focused() && !module.is.visible() && !module.is.empty()
            );
          },
          transition: function() {
            return (
              settings.transition &&
              $.fn.transition !== undefined &&
              $module.transition("is supported")
            );
          }
        },

        is: {
          animating: function() {
            return $results.hasClass(className.animating);
          },
          hidden: function() {
            return $results.hasClass(className.hidden);
          },
          inMessage: function(event) {
            if (!event.target) {
              return;
            }
            var $target = $(event.target),
              isInDOM = $.contains(document.documentElement, event.target);
            return isInDOM && $target.closest(selector.message).length > 0;
          },
          empty: function() {
            return $results.html() === "";
          },
          visible: function() {
            return $results.filter(":visible").length > 0;
          },
          focused: function() {
            return $prompt.filter(":focus").length > 0;
          }
        },

        get: {
          settings: function() {
            if ($.isPlainObject(parameters) && parameters.searchFullText) {
              settings.fullTextSearch = parameters.searchFullText;
              module.error(settings.error.oldSearchSyntax, element);
            }
          },
          inputEvent: function() {
            var prompt = $prompt[0],
              inputEvent =
                prompt !== undefined && prompt.oninput !== undefined
                  ? "input"
                  : prompt !== undefined &&
                    prompt.onpropertychange !== undefined
                    ? "propertychange"
                    : "keyup";
            return inputEvent;
          },
          value: function() {
            return $prompt.val();
          },
          results: function() {
            var results = $module.data(metadata.results);
            return results;
          },
          result: function(value, results) {
            var lookupFields = ["title", "id"],
              result = false;
            value = value !== undefined ? value : module.get.value();
            results = results !== undefined ? results : module.get.results();
            if (settings.type === "category") {
              module.debug("Finding result that matches", value);
              $.each(results, function(index, category) {
                if ($.isArray(category.results)) {
                  result = module.search.object(
                    value,
                    category.results,
                    lookupFields
                  )[0];
                  // don't continue searching if a result is found
                  if (result) {
                    return false;
                  }
                }
              });
            } else {
              module.debug("Finding result in results object", value);
              result = module.search.object(value, results, lookupFields)[0];
            }
            return result || false;
          }
        },

        select: {
          firstResult: function() {
            module.verbose("Selecting first result");
            $result.first().addClass(className.active);
          }
        },

        set: {
          focus: function() {
            $module.addClass(className.focus);
          },
          loading: function() {
            $module.addClass(className.loading);
          },
          value: function(value) {
            module.verbose("Setting search input value", value);
            $prompt.val(value);
          },
          type: function(type) {
            type = type || settings.type;
            if (settings.type == "category") {
              $module.addClass(settings.type);
            }
          },
          buttonPressed: function() {
            $searchButton.addClass(className.pressed);
          }
        },

        remove: {
          loading: function() {
            $module.removeClass(className.loading);
          },
          focus: function() {
            $module.removeClass(className.focus);
          },
          buttonPressed: function() {
            $searchButton.removeClass(className.pressed);
          }
        },

        query: function(callback) {
          callback = $.isFunction(callback) ? callback : function() {};
          var searchTerm = module.get.value(),
            cache = module.read.cache(searchTerm);
          callback = callback || function() {};
          if (module.has.minimumCharacters()) {
            if (cache) {
              module.debug("Reading result from cache", searchTerm);
              module.save.results(cache.results);
              module.addResults(cache.html);
              module.inject.id(cache.results);
              callback();
            } else {
              module.debug("Querying for", searchTerm);
              if (
                $.isPlainObject(settings.source) ||
                $.isArray(settings.source)
              ) {
                module.search.local(searchTerm);
                callback();
              } else if (module.can.useAPI()) {
                module.search.remote(searchTerm, callback);
              } else {
                module.error(error.source);
                callback();
              }
            }
            settings.onSearchQuery.call(element, searchTerm);
          } else {
            module.hideResults();
          }
        },

        search: {
          local: function(searchTerm) {
            var results = module.search.object(searchTerm, settings.content),
              searchHTML;
            module.set.loading();
            module.save.results(results);
            module.debug("Returned full local search results", results);
            if (settings.maxResults > 0) {
              module.debug("Using specified max results", results);
              results = results.slice(0, settings.maxResults);
            }
            if (settings.type == "category") {
              results = module.create.categoryResults(results);
            }
            searchHTML = module.generateResults({
              results: results
            });
            module.remove.loading();
            module.addResults(searchHTML);
            module.inject.id(results);
            module.write.cache(searchTerm, {
              html: searchHTML,
              results: results
            });
          },
          remote: function(searchTerm, callback) {
            callback = $.isFunction(callback) ? callback : function() {};
            if ($module.api("is loading")) {
              $module.api("abort");
            }
            module.setup.api(searchTerm, callback);
            $module.api("query");
          },
          object: function(searchTerm, source, searchFields) {
            var results = [],
              exactResults = [],
              fuzzyResults = [],
              searchExp = searchTerm.toString().replace(regExp.escape, "\\$&"),
              matchRegExp = new RegExp(regExp.beginsWith + searchExp, "i"),
              // avoid duplicates when pushing results
              addResult = function(array, result) {
                var notResult = $.inArray(result, results) == -1,
                  notFuzzyResult = $.inArray(result, fuzzyResults) == -1,
                  notExactResults = $.inArray(result, exactResults) == -1;
                if (notResult && notFuzzyResult && notExactResults) {
                  array.push(result);
                }
              };
            source = source || settings.source;
            searchFields =
              searchFields !== undefined ? searchFields : settings.searchFields;

            // search fields should be array to loop correctly
            if (!$.isArray(searchFields)) {
              searchFields = [searchFields];
            }

            // exit conditions if no source
            if (source === undefined || source === false) {
              module.error(error.source);
              return [];
            }
            // iterate through search fields looking for matches
            $.each(searchFields, function(index, field) {
              $.each(source, function(label, content) {
                var fieldExists = typeof content[field] == "string";
                if (fieldExists) {
                  if (content[field].search(matchRegExp) !== -1) {
                    // content starts with value (first in results)
                    addResult(results, content);
                  } else if (
                    settings.fullTextSearch === "exact" &&
                    module.exactSearch(searchTerm, content[field])
                  ) {
                    // content fuzzy matches (last in results)
                    addResult(exactResults, content);
                  } else if (
                    settings.fullTextSearch == true &&
                    module.fuzzySearch(searchTerm, content[field])
                  ) {
                    // content fuzzy matches (last in results)
                    addResult(fuzzyResults, content);
                  }
                }
              });
            });
            $.merge(exactResults, fuzzyResults);
            $.merge(results, exactResults);
            return results;
          }
        },
        exactSearch: function(query, term) {
          query = query.toLowerCase();
          term = term.toLowerCase();
          if (term.indexOf(query) > -1) {
            return true;
          }
          return false;
        },
        fuzzySearch: function(query, term) {
          var termLength = term.length,
            queryLength = query.length;
          if (typeof query !== "string") {
            return false;
          }
          query = query.toLowerCase();
          term = term.toLowerCase();
          if (queryLength > termLength) {
            return false;
          }
          if (queryLength === termLength) {
            return query === term;
          }
          search: for (
            var characterIndex = 0, nextCharacterIndex = 0;
            characterIndex < queryLength;
            characterIndex++
          ) {
            var queryCharacter = query.charCodeAt(characterIndex);
            while (nextCharacterIndex < termLength) {
              if (term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
                continue search;
              }
            }
            return false;
          }
          return true;
        },

        parse: {
          response: function(response, searchTerm) {
            var searchHTML = module.generateResults(response);
            module.verbose("Parsing server response", response);
            if (response !== undefined) {
              if (
                searchTerm !== undefined &&
                response[fields.results] !== undefined
              ) {
                module.addResults(searchHTML);
                module.inject.id(response[fields.results]);
                module.write.cache(searchTerm, {
                  html: searchHTML,
                  results: response[fields.results]
                });
                module.save.results(response[fields.results]);
              }
            }
          }
        },

        cancel: {
          query: function() {
            if (module.can.useAPI()) {
              $module.api("abort");
            }
          }
        },

        has: {
          minimumCharacters: function() {
            var searchTerm = module.get.value(),
              numCharacters = searchTerm.length;
            return numCharacters >= settings.minCharacters;
          },
          results: function() {
            if ($results.length === 0) {
              return false;
            }
            var html = $results.html();
            return html != "";
          }
        },

        clear: {
          cache: function(value) {
            var cache = $module.data(metadata.cache);
            if (!value) {
              module.debug("Clearing cache", value);
              $module.removeData(metadata.cache);
            } else if (value && cache && cache[value]) {
              module.debug("Removing value from cache", value);
              delete cache[value];
              $module.data(metadata.cache, cache);
            }
          }
        },

        read: {
          cache: function(name) {
            var cache = $module.data(metadata.cache);
            if (settings.cache) {
              module.verbose(
                "Checking cache for generated html for query",
                name
              );
              return typeof cache == "object" && cache[name] !== undefined
                ? cache[name]
                : false;
            }
            return false;
          }
        },

        create: {
          categoryResults: function(results) {
            var categoryResults = {};
            $.each(results, function(index, result) {
              if (!result.category) {
                return;
              }
              if (categoryResults[result.category] === undefined) {
                module.verbose(
                  "Creating new category of results",
                  result.category
                );
                categoryResults[result.category] = {
                  name: result.category,
                  results: [result]
                };
              } else {
                categoryResults[result.category].results.push(result);
              }
            });
            return categoryResults;
          },
          id: function(resultIndex, categoryIndex) {
            var resultID = resultIndex + 1, // not zero indexed
              categoryID = categoryIndex + 1,
              firstCharCode,
              letterID,
              id;
            if (categoryIndex !== undefined) {
              // start char code for "A"
              letterID = String.fromCharCode(97 + categoryIndex);
              id = letterID + resultID;
              module.verbose("Creating category result id", id);
            } else {
              id = resultID;
              module.verbose("Creating result id", id);
            }
            return id;
          },
          results: function() {
            if ($results.length === 0) {
              $results = $("<div />")
                .addClass(className.results)
                .appendTo($module);
            }
          }
        },

        inject: {
          result: function(result, resultIndex, categoryIndex) {
            module.verbose("Injecting result into results");
            var $selectedResult =
              categoryIndex !== undefined
                ? $results
                    .children()
                    .eq(categoryIndex)
                    .children(selector.results)
                    .first()
                    .children(selector.result)
                    .eq(resultIndex)
                : $results.children(selector.result).eq(resultIndex);
            module.verbose("Injecting results metadata", $selectedResult);
            $selectedResult.data(metadata.result, result);
          },
          id: function(results) {
            module.debug("Injecting unique ids into results");
            var // since results may be object, we must use counters
              categoryIndex = 0,
              resultIndex = 0;
            if (settings.type === "category") {
              // iterate through each category result
              $.each(results, function(index, category) {
                resultIndex = 0;
                $.each(category.results, function(index, value) {
                  var result = category.results[index];
                  if (result.id === undefined) {
                    result.id = module.create.id(resultIndex, categoryIndex);
                  }
                  module.inject.result(result, resultIndex, categoryIndex);
                  resultIndex++;
                });
                categoryIndex++;
              });
            } else {
              // top level
              $.each(results, function(index, value) {
                var result = results[index];
                if (result.id === undefined) {
                  result.id = module.create.id(resultIndex);
                }
                module.inject.result(result, resultIndex);
                resultIndex++;
              });
            }
            return results;
          }
        },

        save: {
          results: function(results) {
            module.verbose(
              "Saving current search results to metadata",
              results
            );
            $module.data(metadata.results, results);
          }
        },

        write: {
          cache: function(name, value) {
            var cache =
              $module.data(metadata.cache) !== undefined
                ? $module.data(metadata.cache)
                : {};
            if (settings.cache) {
              module.verbose("Writing generated html to cache", name, value);
              cache[name] = value;
              $module.data(metadata.cache, cache);
            }
          }
        },

        addResults: function(html) {
          if ($.isFunction(settings.onResultsAdd)) {
            if (settings.onResultsAdd.call($results, html) === false) {
              module.debug("onResultsAdd callback cancelled default action");
              return false;
            }
          }
          if (html) {
            $results.html(html);
            module.refreshResults();
            if (settings.selectFirstResult) {
              module.select.firstResult();
            }
            module.showResults();
          } else {
            module.hideResults(function() {
              $results.empty();
            });
          }
        },

        showResults: function(callback) {
          callback = $.isFunction(callback) ? callback : function() {};
          if (resultsDismissed) {
            return;
          }
          if (!module.is.visible() && module.has.results()) {
            if (module.can.transition()) {
              module.debug("Showing results with css animations");
              $results.transition({
                animation: settings.transition + " in",
                debug: settings.debug,
                verbose: settings.verbose,
                duration: settings.duration,
                onComplete: function() {
                  callback();
                },
                queue: true
              });
            } else {
              module.debug("Showing results with javascript");
              $results.stop().fadeIn(settings.duration, settings.easing);
            }
            settings.onResultsOpen.call($results);
          }
        },
        hideResults: function(callback) {
          callback = $.isFunction(callback) ? callback : function() {};
          if (module.is.visible()) {
            if (module.can.transition()) {
              module.debug("Hiding results with css animations");
              $results.transition({
                animation: settings.transition + " out",
                debug: settings.debug,
                verbose: settings.verbose,
                duration: settings.duration,
                onComplete: function() {
                  callback();
                },
                queue: true
              });
            } else {
              module.debug("Hiding results with javascript");
              $results.stop().fadeOut(settings.duration, settings.easing);
            }
            settings.onResultsClose.call($results);
          }
        },

        generateResults: function(response) {
          module.debug("Generating html from response", response);
          var template = settings.templates[settings.type],
            isProperObject =
              $.isPlainObject(response[fields.results]) &&
              !$.isEmptyObject(response[fields.results]),
            isProperArray =
              $.isArray(response[fields.results]) &&
              response[fields.results].length > 0,
            html = "";
          if (isProperObject || isProperArray) {
            if (settings.maxResults > 0) {
              if (isProperObject) {
                if (settings.type == "standard") {
                  module.error(error.maxResults);
                }
              } else {
                response[fields.results] = response[fields.results].slice(
                  0,
                  settings.maxResults
                );
              }
            }
            if ($.isFunction(template)) {
              html = template(response, fields);
            } else {
              module.error(error.noTemplate, false);
            }
          } else if (settings.showNoResults) {
            html = module.displayMessage(error.noResults, "empty");
          }
          settings.onResults.call(element, response);
          return html;
        },

        displayMessage: function(text, type) {
          type = type || "standard";
          module.debug("Displaying message", text, type);
          module.addResults(settings.templates.message(text, type));
          return settings.templates.message(text, type);
        },

        setting: function(name, value) {
          if ($.isPlainObject(name)) {
            $.extend(true, settings, name);
          } else if (value !== undefined) {
            settings[name] = value;
          } else {
            return settings[name];
          }
        },
        internal: function(name, value) {
          if ($.isPlainObject(name)) {
            $.extend(true, module, name);
          } else if (value !== undefined) {
            module[name] = value;
          } else {
            return module[name];
          }
        },
        debug: function() {
          if (!settings.silent && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            } else {
              module.debug = Function.prototype.bind.call(
                console.info,
                console,
                settings.name + ":"
              );
              module.debug.apply(console, arguments);
            }
          }
        },
        verbose: function() {
          if (!settings.silent && settings.verbose && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            } else {
              module.verbose = Function.prototype.bind.call(
                console.info,
                console,
                settings.name + ":"
              );
              module.verbose.apply(console, arguments);
            }
          }
        },
        error: function() {
          if (!settings.silent) {
            module.error = Function.prototype.bind.call(
              console.error,
              console,
              settings.name + ":"
            );
            module.error.apply(console, arguments);
          }
        },
        performance: {
          log: function(message) {
            var currentTime, executionTime, previousTime;
            if (settings.performance) {
              currentTime = new Date().getTime();
              previousTime = time || currentTime;
              executionTime = currentTime - previousTime;
              time = currentTime;
              performance.push({
                Name: message[0],
                Arguments: [].slice.call(message, 1) || "",
                Element: element,
                "Execution Time": executionTime
              });
            }
            clearTimeout(module.performance.timer);
            module.performance.timer = setTimeout(
              module.performance.display,
              500
            );
          },
          display: function() {
            var title = settings.name + ":",
              totalTime = 0;
            time = false;
            clearTimeout(module.performance.timer);
            $.each(performance, function(index, data) {
              totalTime += data["Execution Time"];
            });
            title += " " + totalTime + "ms";
            if (moduleSelector) {
              title += " '" + moduleSelector + "'";
            }
            if ($allModules.length > 1) {
              title += " " + "(" + $allModules.length + ")";
            }
            if (
              (console.group !== undefined || console.table !== undefined) &&
              performance.length > 0
            ) {
              console.groupCollapsed(title);
              if (console.table) {
                console.table(performance);
              } else {
                $.each(performance, function(index, data) {
                  console.log(
                    data["Name"] + ": " + data["Execution Time"] + "ms"
                  );
                });
              }
              console.groupEnd();
            }
            performance = [];
          }
        },
        invoke: function(query, passedArguments, context) {
          var object = instance,
            maxDepth,
            found,
            response;
          passedArguments = passedArguments || queryArguments;
          context = element || context;
          if (typeof query == "string" && object !== undefined) {
            query = query.split(/[\. ]/);
            maxDepth = query.length - 1;
            $.each(query, function(depth, value) {
              var camelCaseValue =
                depth != maxDepth
                  ? value +
                    query[depth + 1].charAt(0).toUpperCase() +
                    query[depth + 1].slice(1)
                  : query;
              if (
                $.isPlainObject(object[camelCaseValue]) &&
                depth != maxDepth
              ) {
                object = object[camelCaseValue];
              } else if (object[camelCaseValue] !== undefined) {
                found = object[camelCaseValue];
                return false;
              } else if ($.isPlainObject(object[value]) && depth != maxDepth) {
                object = object[value];
              } else if (object[value] !== undefined) {
                found = object[value];
                return false;
              } else {
                return false;
              }
            });
          }
          if ($.isFunction(found)) {
            response = found.apply(context, passedArguments);
          } else if (found !== undefined) {
            response = found;
          }
          if ($.isArray(returnedValue)) {
            returnedValue.push(response);
          } else if (returnedValue !== undefined) {
            returnedValue = [returnedValue, response];
          } else if (response !== undefined) {
            returnedValue = response;
          }
          return found;
        }
      };
      if (methodInvoked) {
        if (instance === undefined) {
          module.initialize();
        }
        module.invoke(query);
      } else {
        if (instance !== undefined) {
          instance.invoke("destroy");
        }
        module.initialize();
      }
    });

    return returnedValue !== undefined ? returnedValue : this;
  };

  $.fn.search.settings = {
    name: "Search",
    namespace: "search",

    silent: false,
    debug: false,
    verbose: false,
    performance: true,

    // template to use (specified in settings.templates)
    type: "standard",

    // minimum characters required to search
    minCharacters: 1,

    // whether to select first result after searching automatically
    selectFirstResult: false,

    // API config
    apiSettings: false,

    // object to search
    source: false,

    // Whether search should query current term on focus
    searchOnFocus: true,

    // fields to search
    searchFields: ["title", "description"],

    // field to display in standard results template
    displayField: "",

    // search anywhere in value (set to 'exact' to require exact matches
    fullTextSearch: "exact",

    // whether to add events to prompt automatically
    automatic: true,

    // delay before hiding menu after blur
    hideDelay: 0,

    // delay before searching
    searchDelay: 200,

    // maximum results returned from search
    maxResults: 7,

    // whether to store lookups in local cache
    cache: true,

    // whether no results errors should be shown
    showNoResults: true,

    // transition settings
    transition: "scale",
    duration: 200,
    easing: "easeOutExpo",

    // callbacks
    onSelect: false,
    onResultsAdd: false,

    onSearchQuery: function(query) {},
    onResults: function(response) {},

    onResultsOpen: function() {},
    onResultsClose: function() {},

    className: {
      animating: "animating",
      active: "active",
      empty: "empty",
      focus: "focus",
      hidden: "hidden",
      loading: "loading",
      results: "results",
      pressed: "down"
    },

    error: {
      source:
        "Cannot search. No source used, and Semantic API module was not included",
      noResults: "Your search returned no results",
      logging: "Error in debug logging, exiting.",
      noEndpoint: "No search endpoint was specified",
      noTemplate: "A valid template name was not specified.",
      oldSearchSyntax:
        "searchFullText setting has been renamed fullTextSearch for consistency, please adjust your settings.",
      serverError: "There was an issue querying the server.",
      maxResults: "Results must be an array to use maxResults setting",
      method: "The method you called is not defined."
    },

    metadata: {
      cache: "cache",
      results: "results",
      result: "result"
    },

    regExp: {
      escape: /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
      beginsWith: "(?:s|^)"
    },

    // maps api response attributes to internal representation
    fields: {
      categories: "results", // array of categories (category view)
      categoryName: "name", // name of category (category view)
      categoryResults: "results", // array of results (category view)
      description: "description", // result description
      image: "image", // result image
      price: "price", // result price
      results: "results", // array of results (standard)
      title: "title", // result title
      url: "url", // result url
      action: "action", // "view more" object name
      actionText: "text", // "view more" text
      actionURL: "url" // "view more" url
    },

    selector: {
      prompt: ".prompt",
      searchButton: ".search.button",
      results: ".results",
      message: ".results > .message",
      category: ".category",
      result: ".result",
      title: ".title, .name"
    },

    templates: {
      escape: function(string) {
        var badChars = /[&<>"'`]/g,
          shouldEscape = /[&<>"'`]/,
          escape = {
            "&": "&amp;",
            "<": "&lt;",
            ">": "&gt;",
            '"': "&quot;",
            "'": "&#x27;",
            "`": "&#x60;"
          },
          escapedChar = function(chr) {
            return escape[chr];
          };
        if (shouldEscape.test(string)) {
          return string.replace(badChars, escapedChar);
        }
        return string;
      },
      message: function(message, type) {
        var html = "";
        if (message !== undefined && type !== undefined) {
          html += "" + '<div class="message ' + type + '">';
          // message type
          if (type == "empty") {
            html +=
              "" +
              '<div class="header">No Results</div class="header">' +
              '<div class="description">' +
              message +
              '</div class="description">';
          } else {
            html += ' <div class="description">' + message + "</div>";
          }
          html += "</div>";
        }
        return html;
      },
      category: function(response, fields) {
        var html = "",
          escape = $.fn.search.settings.templates.escape;
        if (response[fields.categoryResults] !== undefined) {
          // each category
          $.each(response[fields.categoryResults], function(index, category) {
            if (
              category[fields.results] !== undefined &&
              category.results.length > 0
            ) {
              html += '<div class="category">';

              if (category[fields.categoryName] !== undefined) {
                html +=
                  '<div class="name">' +
                  category[fields.categoryName] +
                  "</div>";
              }

              // each item inside category
              html += '<div class="results">';
              $.each(category.results, function(index, result) {
                if (result[fields.url]) {
                  html +=
                    '<a class="result" href="' + result[fields.url] + '">';
                } else {
                  html += '<a class="result">';
                }
                if (result[fields.image] !== undefined) {
                  html +=
                    "" +
                    '<div class="image">' +
                    ' <img src="' +
                    result[fields.image] +
                    '">' +
                    "</div>";
                }
                html += '<div class="content">';
                if (result[fields.price] !== undefined) {
                  html +=
                    '<div class="price">' + result[fields.price] + "</div>";
                }
                if (result[fields.title] !== undefined) {
                  html +=
                    '<div class="title">' + result[fields.title] + "</div>";
                }
                if (result[fields.description] !== undefined) {
                  html +=
                    '<div class="description">' +
                    result[fields.description] +
                    "</div>";
                }
                html += "" + "</div>";
                html += "</a>";
              });
              html += "</div>";
              html += "" + "</div>";
            }
          });
          if (response[fields.action]) {
            html +=
              "" +
              '<a href="' +
              response[fields.action][fields.actionURL] +
              '" class="action">' +
              response[fields.action][fields.actionText] +
              "</a>";
          }
          return html;
        }
        return false;
      },
      standard: function(response, fields) {
        var html = "";
        if (response[fields.results] !== undefined) {
          // each result
          $.each(response[fields.results], function(index, result) {
            if (result[fields.url]) {
              html += '<a class="result" href="' + result[fields.url] + '">';
            } else {
              html += '<a class="result">';
            }
            if (result[fields.image] !== undefined) {
              html +=
                "" +
                '<div class="image">' +
                ' <img src="' +
                result[fields.image] +
                '">' +
                "</div>";
            }
            html += '<div class="content">';
            if (result[fields.price] !== undefined) {
              html += '<div class="price">' + result[fields.price] + "</div>";
            }
            if (result[fields.title] !== undefined) {
              html += '<div class="title">' + result[fields.title] + "</div>";
            }
            if (result[fields.description] !== undefined) {
              html +=
                '<div class="description">' +
                result[fields.description] +
                "</div>";
            }
            html += "" + "</div>";
            html += "</a>";
          });

          if (response[fields.action]) {
            html +=
              "" +
              '<a href="' +
              response[fields.action][fields.actionURL] +
              '" class="action">' +
              response[fields.action][fields.actionText] +
              "</a>";
          }
          return html;
        }
        return false;
      }
    }
  };
})(jQuery, window, document);
